spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1import { ImageResponse } from 'next/og';2import { t } from '@/i18n';3import { safe } from '@/lib/api';4import { apiExplore } from '@/lib/api-explore';5import { formatValue } from '@/lib/format';6import { OG_INK2, OG_INK3, OG_RULE, OG_SIZE_H, OG_SIZE_W, OgFrame, OgWordmark } from '../../og-shared';78export const alt = 'Indicator by country on CountryAtlas';9export const size = { width: OG_SIZE_W, height: OG_SIZE_H };10export const contentType = 'image/png';11export const revalidate = 3600;1213/** Light→dark sequential ramp (globals.css --seq-1…7, light theme) — Satori needs literal colours. */14const SEQ = ['#cde2fb', '#9ec5f4', '#6da7ec', '#3987e5', '#256abf', '#184f95', '#0d366b'];1516/** Per-indicator social image: name, world value + kind, year, coverage, mini sequential legend. */17export default async function IndicatorOgImage({ params }: { params: Promise<{ slug: string }> }) {18 const { slug } = await params;19 const data = await safe(apiExplore.indicator(slug));20 const ind = data?.indicator;21 const name = ind?.name ?? slug.replace(/-/g, ' ');22 const wl = data?.world_latest ?? null;23 const kind = wl ? (wl.kind === 'sum' ? 'World total' : wl.kind === 'weighted_mean' ? 'World average' : 'World median') : null;24 const sub = [ind?.unit, ind?.topic ? ind.topic.replace(/-/g, ' ') : null].filter(Boolean).join(' · ');25 const coverage = data ? `${data.coverage.n_countries ?? 0} countries · ${data.years.first ?? ''}–${data.years.last_actual ?? ''}` : '';26 return new ImageResponse(27 (28 <OgFrame>29 <OgWordmark size={28} />30 <div style={{ display: 'flex', flexDirection: 'column', marginTop: 48, maxWidth: 900 }}>31 <div style={{ fontSize: 22, color: OG_INK3, textTransform: 'uppercase', letterSpacing: 2, display: 'flex' }}>{t('common.indicator')}</div>32 <div style={{ fontSize: name.length > 40 ? 52 : 64, fontWeight: 600, letterSpacing: -1.5, lineHeight: 1.05, marginTop: 8 }}>{name}</div>33 {sub ? <div style={{ marginTop: 10, fontSize: 24, color: OG_INK2 }}>{sub}</div> : null}34 </div>35 <div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', marginTop: 'auto', paddingTop: 24, borderTop: `1px solid ${OG_RULE}` }}>36 {wl && ind && wl.value != null ? (37 <div style={{ display: 'flex', flexDirection: 'column' }}>38 <div style={{ fontSize: 18, color: OG_INK3, textTransform: 'uppercase', letterSpacing: 1.5 }}>{kind}</div>39 <div style={{ fontSize: 52, fontWeight: 600, marginTop: 6 }}>{formatValue(wl.value, ind)}</div>40 <div style={{ fontSize: 18, color: OG_INK3, marginTop: 2 }}>{`${wl.year ?? ''}${coverage ? ` · ${coverage}` : ''}`}</div>41 </div>42 ) : (43 <div style={{ fontSize: 24, color: OG_INK2 }}>{coverage || 'World map, trend, ranking and sources'}</div>44 )}45 <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>46 <div style={{ display: 'flex', gap: 3 }}>47 {SEQ.map((c) => (48 <div key={c} style={{ width: 34, height: 14, background: c, borderRadius: 2 }} />49 ))}50 </div>51 <div style={{ display: 'flex', justifyContent: 'space-between', width: 256, marginTop: 6, fontSize: 16, color: OG_INK3 }}>52 <span>low</span>53 <span>high</span>54 </div>55 </div>56 </div>57 <div style={{ position: 'absolute', right: 64, bottom: 24, fontSize: 20, color: OG_INK3 }}>{t('og.site')}</div>58 </OgFrame>59 ),60 { ...size },61 );62}63